import { IEvent } from '@feng3d/event';
import { $set } from '@feng3d/serialization';
import { AnimationAction3D, AnimationMixer3D, Clock, Color, Fog3D, MeshPhongMaterial3D, Node3D, PerspectiveCamera3D, PlaneGeometry3D, Scene3D, SkeletonHelper3D, StatsComponent, WebGLRenderer3D } from 'feng3d-three';
import GUI from 'lil-gui';
import { GLTFLoader3D } from '../../extension/loaders/GLTFLoader3D';

const gltfUrl = '../../models/gltf/Soldier.glb';

const settings = {
    'show model': true,
    'show skeleton': false,
    'deactivate all': deactivateAllActions,
    'activate all': activateAllActions,
    'pause/continue': pauseContinue,
    'make single step': toSingleStepMode,
    'modify step size': 0.05,
    'from walk to idle'()
    {
        prepareCrossFade(walkAction, idleAction, 1.0);
    },
    'from idle to walk'()
    {
        prepareCrossFade(idleAction, walkAction, 0.5);
    },
    'from walk to run'()
    {
        prepareCrossFade(walkAction, runAction, 2.5);
    },
    'from run to walk'()
    {
        prepareCrossFade(runAction, walkAction, 5.0);
    },
    'use default duration': true,
    'set custom duration': 3.5,
    'modify idle weight': 0.0,
    'modify walk weight': 1.0,
    'modify run weight': 0.0,
    'modify time scale': 1.0
};

const root = new Node3D();

let scene: Scene3D;
let renderer: WebGLRenderer3D;
let camera: PerspectiveCamera3D;
let stats: StatsComponent;
let model: Node3D;
let skeleton: SkeletonHelper3D;
let mixer: AnimationMixer3D;
let clock: Clock;

const crossFadeControls = [];

let idleAction: AnimationAction3D;
let walkAction: AnimationAction3D;
let runAction: AnimationAction3D;
let idleWeight: number;
let walkWeight: number;
let runWeight: number;
let actions: AnimationAction3D[];

let singleStepMode = false;
let sizeOfNextStep = 0;

init();

function init()
{
    const container = document.getElementById('container');

    camera = new Node3D().addComponent('PerspectiveCamera3D', { fov: 45, aspect: window.innerWidth / window.innerHeight, near: 1, far: 1000 });
    camera.entity.position.set(1, 2, -3);
    camera.entity.lookAt(0, 1, 0);

    clock = new Clock();

    scene = root.addComponent('Scene3D');
    scene.background = new Color(0xa0a0a0);
    scene.fog = new Fog3D(0xa0a0a0, 10, 50);

    const hemiLight = new Node3D().addComponent('HemisphereLight3D', { color: { value: 0xffffff }, groundColor: { value: 0x444444 } });
    hemiLight.entity.position.set(0, 20, 0);
    scene.entity.addChild(hemiLight.entity);

    const dirLight = new Node3D().addComponent('DirectionalLight3D', { color: { value: 0xffffff } });
    dirLight.entity.position.set(-3, 10, -10);
    dirLight.castShadow = true;
    dirLight.shadow.camera.top = 2;
    dirLight.shadow.camera.bottom = -2;
    dirLight.shadow.camera.left = -2;
    dirLight.shadow.camera.right = 2;
    dirLight.shadow.camera.near = 0.1;
    dirLight.shadow.camera.far = 40;
    scene.entity.addChild(dirLight.entity);

    scene.entity.addChild(new Node3D().addComponent('CameraHelper3D', { camera: dirLight.shadow.camera }).entity);

    // ground

    const mesh = new Node3D().addComponent('Mesh3D', {
        geometry: $set(new PlaneGeometry3D(), { width: 100, height: 100 }),
        material: $set(new MeshPhongMaterial3D(), { color: { value: 0x999999 }, depthWrite: false }),
    });
    mesh.entity.rotation.x = -Math.PI / 2;
    mesh.receiveShadow = true;
    scene.entity.addChild(mesh.entity);

    const loader = new GLTFLoader3D();
    loader.load(gltfUrl, function (gltf)
    {
        model = gltf.scene;
        scene.entity.addChild(model);

        model._group.traverse(function (object: any)
        {
            if (object.isMesh) object.castShadow = true;
        });

        //

        skeleton = new Node3D().addComponent('SkeletonHelper3D', { node3D: model });
        skeleton.entity.visible = false;
        scene.entity.addChild(skeleton.entity);

        //

        createPanel();

        //

        const animations = gltf.animations;

        mixer = new AnimationMixer3D(model);

        idleAction = mixer.clipAction(animations[0]);
        walkAction = mixer.clipAction(animations[3]);
        runAction = mixer.clipAction(animations[1]);

        actions = [idleAction, walkAction, runAction];

        activateAllActions();

        animate();
    });

    renderer = root.addComponent('WebGLRenderer3D', {
        parameters: { antialias: true },
        pixelRatio: window.devicePixelRatio,
        size: { x: window.innerWidth, y: window.innerHeight },
    });
    renderer.outputEncoding = 'sRGBEncoding';
    renderer.shadowMap.enabled = true;
    container.appendChild(renderer.domElement);

    stats = root.addComponent('StatsComponent');
    container.appendChild(stats.dom);

    window.addEventListener('resize', onWindowResize);
}

function createPanel()
{
    const panel = new GUI({ width: 310 });

    const folder1 = panel.addFolder('Visibility');
    const folder2 = panel.addFolder('Activation/Deactivation');
    const folder3 = panel.addFolder('Pausing/Stepping');
    const folder4 = panel.addFolder('Crossfading');
    const folder5 = panel.addFolder('Blend Weights');
    const folder6 = panel.addFolder('General Speed');

    folder1.add(settings, 'show model').onChange(showModel);
    folder1.add(settings, 'show skeleton').onChange(showSkeleton);
    folder2.add(settings, 'deactivate all');
    folder2.add(settings, 'activate all');
    folder3.add(settings, 'pause/continue');
    folder3.add(settings, 'make single step');
    folder3.add(settings, 'modify step size', 0.01, 0.1, 0.001);
    crossFadeControls.push(folder4.add(settings, 'from walk to idle'));
    crossFadeControls.push(folder4.add(settings, 'from idle to walk'));
    crossFadeControls.push(folder4.add(settings, 'from walk to run'));
    crossFadeControls.push(folder4.add(settings, 'from run to walk'));
    folder4.add(settings, 'use default duration');
    folder4.add(settings, 'set custom duration', 0, 10, 0.01);
    folder5.add(settings, 'modify idle weight', 0.0, 1.0, 0.01).listen().onChange(function (weight)
    {
        setWeight(idleAction, weight);
    });
    folder5.add(settings, 'modify walk weight', 0.0, 1.0, 0.01).listen().onChange(function (weight)
    {
        setWeight(walkAction, weight);
    });
    folder5.add(settings, 'modify run weight', 0.0, 1.0, 0.01).listen().onChange(function (weight)
    {
        setWeight(runAction, weight);
    });
    folder6.add(settings, 'modify time scale', 0.0, 1.5, 0.01).onChange(modifyTimeScale);

    folder1.open();
    folder2.open();
    folder3.open();
    folder4.open();
    folder5.open();
    folder6.open();
}

function showModel(visibility: boolean)
{
    model.visible = visibility;
}

function showSkeleton(visibility: boolean)
{
    skeleton.entity.visible = visibility;
}

function modifyTimeScale(speed: number)
{
    mixer.timeScale = speed;
}

function deactivateAllActions()
{
    actions.forEach(function (action)
    {
        action.stop();
    });
}

function activateAllActions()
{
    setWeight(idleAction, settings['modify idle weight']);
    setWeight(walkAction, settings['modify walk weight']);
    setWeight(runAction, settings['modify run weight']);

    actions.forEach(function (action)
    {
        action.play();
    });
}

function pauseContinue()
{
    if (singleStepMode)
    {
        singleStepMode = false;
        unPauseAllActions();
    }
    else if (idleAction.paused)
    {
        unPauseAllActions();
    }
    else
    {
        pauseAllActions();
    }
}

function pauseAllActions()
{
    actions.forEach(function (action)
    {
        action.paused = true;
    });
}

function unPauseAllActions()
{
    actions.forEach(function (action)
    {
        action.paused = false;
    });
}

function toSingleStepMode()
{
    unPauseAllActions();

    singleStepMode = true;
    sizeOfNextStep = settings['modify step size'];
}

function prepareCrossFade(startAction: AnimationAction3D, endAction: AnimationAction3D, defaultDuration: number)
{
    // Switch default / custom crossfade duration (according to the user's choice)

    const duration = setCrossFadeDuration(defaultDuration);

    // Make sure that we don't go on in singleStepMode, and that all actions are unpaused

    singleStepMode = false;
    unPauseAllActions();

    // If the current action is 'idle' (duration 4 sec), execute the crossfade immediately;
    // else wait until the current action has finished its current loop

    if (startAction === idleAction)
    {
        executeCrossFade(startAction, endAction, duration);
    }
    else
    {
        synchronizeCrossFade(startAction, endAction, duration);
    }
}

function setCrossFadeDuration(defaultDuration: number)
{
    // Switch default crossfade duration <-> custom crossfade duration

    if (settings['use default duration'])
    {
        return defaultDuration;
    }

    return settings['set custom duration'];
}

function synchronizeCrossFade(startAction: AnimationAction3D, endAction: AnimationAction3D, duration: number)
{
    mixer.on('loop', onLoopFinished);

    function onLoopFinished(event: IEvent<{ action: AnimationAction3D, loopDelta: number }>)
    {
        if (event.data.action === startAction)
        {
            mixer.off('loop', onLoopFinished);

            executeCrossFade(startAction, endAction, duration);
        }
    }
}

function executeCrossFade(startAction: AnimationAction3D, endAction: AnimationAction3D, duration: number)
{
    // Not only the start action, but also the end action must get a weight of 1 before fading
    // (concerning the start action this is already guaranteed in this place)

    setWeight(endAction, 1);
    endAction.time = 0;

    // Crossfade with warping - you can also try without warping by setting the third parameter to false

    startAction.crossFadeTo(endAction, duration, true);
}

// This function is needed, since animationAction.crossFadeTo() disables its start action and sets
// the start action's timeScale to ((start animation's duration) / (end animation's duration))

function setWeight(action: AnimationAction3D, weight: number)
{
    action.enabled = true;
    action.setEffectiveTimeScale(1);
    action.setEffectiveWeight(weight);
}

// // Called by the render loop

function updateWeightSliders()
{
    settings['modify idle weight'] = idleWeight;
    settings['modify walk weight'] = walkWeight;
    settings['modify run weight'] = runWeight;
}

// Called by the render loop

function updateCrossFadeControls()
{
    if (idleWeight === 1 && walkWeight === 0 && runWeight === 0)
    {
        crossFadeControls[0].disable();
        crossFadeControls[1].enable();
        crossFadeControls[2].disable();
        crossFadeControls[3].disable();
    }

    if (idleWeight === 0 && walkWeight === 1 && runWeight === 0)
    {
        crossFadeControls[0].enable();
        crossFadeControls[1].disable();
        crossFadeControls[2].enable();
        crossFadeControls[3].disable();
    }

    if (idleWeight === 0 && walkWeight === 0 && runWeight === 1)
    {
        crossFadeControls[0].disable();
        crossFadeControls[1].disable();
        crossFadeControls[2].disable();
        crossFadeControls[3].enable();
    }
}

function onWindowResize()
{
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();

    renderer.setSize(window.innerWidth, window.innerHeight);
}

function animate()
{
    // Render loop

    requestAnimationFrame(animate);

    idleWeight = idleAction.getEffectiveWeight();
    walkWeight = walkAction.getEffectiveWeight();
    runWeight = runAction.getEffectiveWeight();

    // Update the panel values if weights are modified from "outside" (by crossfadings)

    updateWeightSliders();

    // Enable/disable crossfade controls according to current weight values

    updateCrossFadeControls();

    // Get the time elapsed since the last frame, used for mixer update (if not in single step mode)

    let mixerUpdateDelta = clock.getDelta();

    // If in single step mode, make one step and then do nothing (until the user clicks again)

    if (singleStepMode)
    {
        mixerUpdateDelta = sizeOfNextStep;
        sizeOfNextStep = 0;
    }

    // Update the animation mixer, the stats panel, and render this frame

    mixer.update(mixerUpdateDelta);

    stats.update();

    renderer.render(scene, camera);
}
